using System;
using System.Collections.Generic;
using System.Text;
using System.Data;
using System.Reflection;
using Decav.Reality;
using System.Net.Mail;
using System.IO;
using System.Xml;
using Decav.Financial;

namespace Decav.Data
{
    /// <summary>
    /// An interface to creating an object from a data store, and supplying its constructor
    /// with its internal values.
    /// </summary>
    public class DataRecordAdapter
    {
        /// <summary>
        /// Initializes a new <see cref="DataRecordAdapter"/> for a data record.
        /// </summary>
        /// <param name="record">The record to intialize the adapter for.</param>
        public DataRecordAdapter(IDataRecord record)
        {
            if (record == null)
                throw new ArgumentNullException("record");

            _record = record;
        }

        private IDataRecord _record;
        private readonly Stack<string> _prefixes = new Stack<string>();

        /// <summary>
        /// Adds a prefix to prefix all the column names by when reading, until the <see cref="PopPrefix"/>
        /// operation is called.
        /// </summary>
        /// <param name="prefix">The prefix to add to the beginning of all column anmes.</param>
        public void PushPrefix(string prefix)
        {
            if (prefix == null)
                throw new ArgumentNullException("prefix");

            _prefixes.Push(prefix);
        }

        /// <summary>
        /// Removes the last prefix appended to the column names using <see cref="PushPrefix"/>.
        /// </summary>
        public void PopPrefix()
        {
            if (_prefixes.Count == 0)
                throw new InvalidOperationException("Cannot remove a prefix from the DataRecordAdapter because no prefixes are specified.");

            _prefixes.Pop();
        }

        /// <summary>
        /// Gets the current prefix applied to column names when reading.
        /// </summary>
        public string CurrentPrefix
        {
            get
            {
                return string.Join(string.Empty, _prefixes.ToArray());
            }
        }

        /// <summary>
        /// Gets a value for the specified type name.
        /// </summary>
        /// <typeparam name="T">The type to get.</typeparam>
        /// <param name="valueName">The name of the column holding the value.</param>
        /// <returns>The requested value.</returns>
        public T GetValue<T>(string valueName)
        {
            if (valueName == null)
                throw new ArgumentNullException("valueName");

            if (_prefixes.Count > 0)
                valueName = string.Format("{0}{1}", CurrentPrefix, valueName);

            int ordinal = _record.GetOrdinal(valueName);

            if (ordinal == -1)
                throw new ObjectBuildException(string.Format("Cannot get the value for column '{0}', the column does not exist.", valueName));

            if (_record.IsDBNull(ordinal))
                return default(T);

            object value = _record.GetValue(ordinal);

            if (!value.GetType().IsAssignableFrom(typeof(T)))
                throw new ObjectBuildException(string.Format("Cannot get value from column '{0}' as type of '{1}'.", valueName, typeof(T).FullName));

            return (T)value;
        }

        /// <summary>
        /// Checks if the <see cref="DataRecordAdapter"/> has a value for the specified column.
        /// </summary>
        /// <param name="valueName">The name of the column to check for.</param>
        /// <returns>True when the column exists, else false.</returns>
        public bool HasValue(string valueName)
        {
            if (valueName == null)
                throw new ArgumentNullException("valueName");

            if (_prefixes.Count > 0)
                valueName = string.Format("{0}{1}", CurrentPrefix, valueName);

            for (int i = 0; i < _record.FieldCount; i++)
            {
                if (valueName.ToLower() == _record.GetName(i).ToLower())
                    return true;
            }

            return false;
        }

        /// <summary>
        /// Creates a <see cref="StreetAddress"/> using a prefix and the current adapters data.
        /// </summary>
        /// <param name="prefix">The prefix to prepend to the column name, or null if no prefix
        /// should be prepended.</param>
        /// <returns>The street address from the current adapter.</returns>
        public StreetAddress GetStreetAddress(string prefix)
        {
            if (prefix != null)
                PushPrefix(prefix);

            StreetAddress address = DataRecordAdapter.CreateInstance<StreetAddress>(this);

            if (prefix != null)
                PopPrefix();

            return address;
        }

        /// <summary>
        /// Gets a street address from an XML string.
        /// </summary>
        /// <param name="xmlValueName">The name of the field containing the XML.</param>
        /// <returns>The Address.</returns>
        public StreetAddress GetStreetAddressFromXml(string xmlValueName)
        {
            string xml = GetValue<string>(xmlValueName);

            using (StringReader rdr = new StringReader(xml))
            using (XmlReader xmlrdr = XmlReader.Create(rdr))
            {
                return XmlSerializationHelper.BuildObject<StreetAddress>(xmlrdr);
            }
        }

        public PhoneNumber GetPhoneNumber(string valueName)
        {
            string telNum = GetValue<string>(valueName);

            if (telNum == null)
                return PhoneNumber.Empty;

            return new PhoneNumber(telNum);
        }

        public EmailAddress GetEmailAddress(string valueName)
        {
            string value = GetValue<string>(valueName);

            if (value == null || value.Trim().Length == 0)
                return EmailAddress.Empty;

            return new EmailAddress(value);
        }

        public bool GetBoolean(string valueName)
        {
            return GetValue<bool>(valueName);
        }

        public byte GetByte(string valueName)
        {
            return GetValue<byte>(valueName);
        }

        public char GetChar(string valueName)
        {
            return GetValue<char>(valueName);
        }

        public Decav.Geography.LatLongPoint GetLatLongPoint(string valueName)
        {
            if (valueName == null)
                throw new ArgumentNullException("valueName");

            if (_record.IsDBNull(_record.GetOrdinal(valueName)))
                return Decav.Geography.LatLongPoint.Empty;

            LatLongPoint point = (LatLongPoint)_record[valueName];
            return new Decav.Geography.LatLongPoint((decimal)point.Latitude, (decimal)point.Longitude);
        }

        public DateTime GetDateTime(string valueName)
        {
            return GetValue<DateTime>(valueName);
        }

        public decimal GetDecimal(string valueName)
        {
            return GetValue<decimal>(valueName);
        }

        public double GetDouble(string valueName)
        {
            return GetValue<double>(valueName);
        }

        public float GetFloat(string valueName)
        {
            return GetValue<float>(valueName);
        }

        public Guid GetGuid(string valueName)
        {
            return GetValue<Guid>(valueName);
        }

        public short GetInt16(string valueName)
        {
            return GetValue<short>(valueName);
        }

        public int GetInt32(string valueName)
        {
            return GetValue<int>(valueName);
        }

        public long GetInt64(string valueName)
        {
            return GetValue<long>(valueName);
        }

        public string GetString(string valueName)
        {
            return GetValue<string>(valueName);
        }

        public object GetValue(string valueName)
        {
            return GetValue<object>(valueName);
        }

        /// <summary>
        /// Creates a new object of the specified type using the <paramref name="adapter"/>
        /// as data for the object creation.
        /// </summary>
        /// <remarks>
        /// Objects designed for dynamic creation using this method should define a constructor that accepts
        /// a <see cref="DataRecordAdapter"/> that is either protected or private.
        /// <c>
        /// public class MyObject
        /// {
        ///     protected MyObject(DataRecordAdapter adapt)
        ///     {
        ///         _int = adapt.GetInt32("int");
        ///     }
        /// 
        ///     private int _int;
        /// }
        /// </c>
        /// </remarks>
        /// <typeparam name="TObject">The type of object to create.</typeparam>
        /// <param name="adapter">The adapter to pass to the constructor of the object.</param>
        /// <returns>The object fully constructed.</returns>
        /// <exception cref="ObjectBuildException">Thrown when the object fails to build.</exception>
        public static TObject CreateInstance<TObject>(DataRecordAdapter adapter)
        {
            if ( adapter == null )
                throw new ArgumentNullException( "adapter" );
        
            Type objType = typeof(TObject);

            ConstructorInfo ctor = objType.GetConstructor(BindingFlags.NonPublic | BindingFlags.Instance, null, _builderTypes, null);

            if (ctor == null)
                throw new ObjectBuildException("The object type could not be created because no constructor is defined to consume a DataRecordAdapter.");

            TObject obj = default(TObject);
            try
            {
                obj = (TObject)ctor.Invoke(new object[] { adapter });
            }
            catch (TargetInvocationException ex)
            {
                throw new ObjectBuildException(
                    string.Format("The object could not be built because an exception was thrown from the constructor.\r\n\r\n{0}.",
                    ex.InnerException.Message), ex);
            }

            return obj;
        }

        /// <summary>
        /// Creates a new object of the specified type using the <paramref name="record"/>
        /// as data for the object creation.
        /// </summary>
        /// <remarks>
        /// Objects designed for dynamic creation using this method should define a constructor that accepts
        /// a <see cref="DataRecordAdapter"/> that is either protected or private.
        /// <c>
        /// public class MyObject
        /// {
        ///     protected MyObject(DataRecordAdapter adapt)
        ///     {
        ///         _int = adapt.GetInt32("int");
        ///     }
        /// 
        ///     private int _int;
        /// }
        /// </c>
        /// </remarks>
        /// <typeparam name="TObject">The type of object to create.</typeparam>
        /// <param name="record">The data record to pass to the constructor of the object.</param>
        /// <returns>The object fully constructed.</returns>
        /// <exception cref="ObjectBuildException">Thrown when the object fails to build.</exception>
        public static TObject CreateInstance<TObject>(IDataRecord record)
        {
            if (record == null)
                throw new ArgumentNullException("record");

            return CreateInstance<TObject>(new DataRecordAdapter(record));
        }

        private static readonly Type[] _builderTypes = new Type[] { typeof(DataRecordAdapter) };

        /// <summary>
        /// Gets a <see cref="Money"/> value from the data record.
        /// </summary>
        /// <param name="valueColumn">The name of the column containing the value.</param>
        /// <param name="currencyColumn">The name of the column containing the three letter currency code.</param>
        /// <returns>A money value.</returns>
        public Money GetMoney(string valueColumn, string currencyColumn)
        {
            if (valueColumn == null)
                throw new ArgumentNullException("valueColumn");

            if (currencyColumn == null)
                throw new ArgumentNullException("currencyColumn");

            return new Money(MoneyCurrency.GetCurrency(GetString(currencyColumn)), GetDecimal(valueColumn));
        }
    }
}
